Skip to content

fix(credential): forward [id] positional as credential id via PUT#48

Merged
shreemaan-abhishek merged 3 commits into
masterfrom
fix/credential-create-forward-id-via-put
May 26, 2026
Merged

fix(credential): forward [id] positional as credential id via PUT#48
shreemaan-abhishek merged 3 commits into
masterfrom
fix/credential-create-forward-id-via-put

Conversation

@shreemaan-abhishek
Copy link
Copy Markdown
Contributor

@shreemaan-abhishek shreemaan-abhishek commented May 26, 2026

Summary

  • Closes credential: positional [id] is ignored — clarify or fix #36. Supersedes fix(credential): drop misleading [id] positional from create #46 (which dropped [id]; closed in favor of this).
  • a7 credential create <id> ... previously POSTed and the server returned a fresh UUID, contradicting the help text. After auditing api7-ee-3 control-plane, PUT /apisix/admin/consumers/<u>/credentials/<id> is an upsert that honors a client-supplied id (interceptor.go:386-400; DAO OnConflict{UpdateAll}; BeforeCreate only generates a UUID when CredentialID is empty).
  • This PR forwards the positional [id] (and id: inside a --file payload) as the URL path on PUT. When omitted, POST + server-assigned UUID is unchanged.
  • A new --name flag carries the display name, no longer conflated with id.

Precedence

  • Positional [id] overrides payload["id"] from --file (kubectl/gh convention).
  • Non-string id: in a file payload is rejected client-side with a clear error.

Test plan

  • go test ./pkg/cmd/credential/... — pass. New/updated cases:
    • TestCreateCredential_PositionalForwardsAsIDViaPUT
    • TestCreateCredential_NameFlagPOSTs
    • TestCreateCredential_FileIDForwardedViaPUT
    • TestCreateCredential_PositionalOverridesFileID
    • TestCreateCredential_FileRejectsInvalidID
  • go test ./... — pass.
  • go build ./... — clean.
  • Manual smoke against API7 EE:
    • a7 credential create my-key --consumer alice -g default --plugins-json '{"key-auth":{"key":"k"}}' — returns id: my-key.
    • a7 credential create --consumer alice -g default --name "Jack key" --plugins-json '...' — returns server-assigned UUID, name set.
    • a7 credential create -f cred.yaml --consumer alice -g default where cred.yaml has id: from-file — credential created at from-file.

Docs (docs/user-guide/credential.md) and docs/ga-test-report.md updated.

Note: make lint surfaces a pre-existing yaml typecheck error in pkg/cmdutil and pkg/cmd/config/configutil — reproducible on plain master, unrelated to this change.

Summary by CodeRabbit

  • New Features

    • Added a --name flag to set credential display name on creation.
  • Bug Fixes

    • Positional argument now forwards as credential ID for create requests; server UUID used only when no ID provided.
    • Validation tightened for empty/invalid IDs and names.
  • Documentation

    • Rewrote credential creation docs with clearer semantics, examples, ID-precedence rules, and default output format update.
  • Tests

    • Added/updated tests covering positional vs. file ID precedence, name derivation, validation, and URL encoding.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

📝 Walkthrough

Walkthrough

The PR forwards the optional positional [id] as the credential id via PUT, adds a --name flag to set the display name, centralizes POST/PUT request selection and raw JSON output in a submit helper, updates file-payload id/name validation, and adjusts tests and documentation to the new semantics.

Changes

Credential Create: Positional ID Forwarding and Display Name Flag

Layer / File(s) Summary
CLI options and --name flag
pkg/cmd/credential/create/create.go
Adds Options.Name, updates command description, and registers --name for credential display name.
Request building, file handling, and submit helper
pkg/cmd/credential/create/create.go
Selects effective id from positional or file, removes id from outbound JSON when forwarding via URL, computes/validates name (from --name or mirrored id), centralizes POST vs PUT routing, escapes URL path elements, and writes raw JSON response through exporter.
Updated test suite for PUT-id behavior, name rules, and escaping
pkg/cmd/credential/create/create_test.go
Adds/updates tests asserting PUT to /credentials/{id} with no id in body, name derivation (positional/file or --name), positional precedence over file id, validation/trim behavior, and URL escaping.
User guide and GA test report updates
docs/user-guide/credential.md, docs/ga-test-report.md
Documents optional positional [id], precedence rules between positional and file id:, adds examples and flags (--name, --desc, --plugins-json, --labels), sets default --output to json, and updates GA report findings to reflect the fix and --name addition.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • api7/a7#38: Overlaps in docs/ga-test-report.md Run 2 findings and credential positional-id narrative.

Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Security Check ❌ Error Non-file flow doesn't reject whitespace-only --name. File flow validates via stringField() line 111, but non-file flow lines 139-145 allows empty name after trim without error. Add validation in non-file flow (line 140): check if opts.Name is non-empty but trimmed name is empty, return error "credential name must be a non-empty string".
E2e Test Quality Review ⚠️ Warning Review comment unaddressed: whitespace-only --name in non-file flow not validated, allowing empty name submission (file flow correctly rejects this). Tests are mocked unit tests, not E2E tests. Add validation after trimming --name (lines 140-144): reject if opts.Name provided but empty after trimming. Add test for this case. Consider expanding E2E test coverage for PUT/ID forwarding behavior.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly describes the main change: forwarding the [id] positional argument as credential id via PUT request, which is the core behavioral fix addressed in this PR.
Linked Issues check ✅ Passed The PR fulfills issue #36 by implementing option (1): forwarding positional [id] as credential id via PUT when provided, with server-assigned UUID when omitted, including file-id handling and validation.
Out of Scope Changes check ✅ Passed All changes are within scope: credential create behavior refactoring (code), test coverage updates, and documentation updates align with the PR objective of fixing positional id forwarding.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/credential-create-forward-id-via-put

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
pkg/cmd/credential/create/create_test.go (1)

36-120: ⚡ Quick win

Add YAML output coverage for the new shared submit path.

The new submit output branch is exercised only with JSON assertions right now. Please add at least one -o yaml case (POST or PUT) to cover the formatting path introduced in this PR.

As per coding guidelines: **/*.go: “Write tests for every code change”.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/cmd/credential/create/create_test.go` around lines 36 - 120, Add a new
unit test (e.g., TestCreateCredential_YAMLOutput) mirroring
TestCreateCredential_JSONOutput but exercising the shared submit path with YAML
formatting: create an httpmock.Registry responder for the same POST/PUT used in
the other tests, build an Options struct like in the existing tests but set the
Options output field to request YAML (set the Options.Output or
Options.OutputFormat field to "yaml" to mirror how the CLI exposes -o yaml),
call actionRun(opts), unmarshal/parse out.Bytes() as YAML and assert key fields
(id, name/desc) match the mock response, and call registry.Verify(t). Ensure you
reuse Options fields such as IO, Client, Config, Consumer, GatewayGroup,
PluginsJSON and reference actionRun and Options to find where to add the test.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/cmd/credential/create/create.go`:
- Around line 149-153: In submit(), consumer (opts.Consumer) and id are being
interpolated directly into the path and ggID into the query—escape them first to
prevent path/query injection: use url.PathEscape for path segments (consumer and
id used in the path in the fmt.Sprintf inside submit) and url.QueryEscape for
ggID used in the ?gateway_group_id= query string, then build the fmt.Sprintf
with the escaped values before calling client.Put or client.Post; update the two
call sites in submit() accordingly.

---

Nitpick comments:
In `@pkg/cmd/credential/create/create_test.go`:
- Around line 36-120: Add a new unit test (e.g.,
TestCreateCredential_YAMLOutput) mirroring TestCreateCredential_JSONOutput but
exercising the shared submit path with YAML formatting: create an
httpmock.Registry responder for the same POST/PUT used in the other tests, build
an Options struct like in the existing tests but set the Options output field to
request YAML (set the Options.Output or Options.OutputFormat field to "yaml" to
mirror how the CLI exposes -o yaml), call actionRun(opts), unmarshal/parse
out.Bytes() as YAML and assert key fields (id, name/desc) match the mock
response, and call registry.Verify(t). Ensure you reuse Options fields such as
IO, Client, Config, Consumer, GatewayGroup, PluginsJSON and reference actionRun
and Options to find where to add the test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 59f5d968-8ca7-4a9a-abbf-74950f0bbca9

📥 Commits

Reviewing files that changed from the base of the PR and between 3fbbb89 and b47eefd.

📒 Files selected for processing (4)
  • docs/ga-test-report.md
  • docs/user-guide/credential.md
  • pkg/cmd/credential/create/create.go
  • pkg/cmd/credential/create/create_test.go

Comment thread pkg/cmd/credential/create/create.go
API7 EE control-plane upserts on PUT /apisix/admin/consumers/<u>/credentials/<id>
(middleware: interceptor.go:386-400 builds the credential from the body
when GetCredential returns NotFound and the path matches; DAO uses
OnConflict{UpdateAll} keyed on (gateway_group_id, credential_id); the
BeforeCreate GORM hook only auto-generates a UUID when the client did
not supply one). So we can honor the positional id end-to-end.

Before this change, `a7 credential create my-id ...` POSTed with the
positional placed in `name` and returned a server-assigned UUID,
contradicting the help text and breaking scripts that expect their
chosen id back.

After:
- positional `[id]` and `id:` in `--file` payloads are forwarded as the
  URL path on PUT. Positional wins on conflict.
- Server assigns the id when the positional is omitted (POST path
  unchanged).
- New `--name` flag carries the display name, no longer conflated with
  id.
- Tests cover: PUT-with-positional, PUT-with-file-id,
  positional-overrides-file, --name on POST, and invalid-id rejection.

Docs and the GA test report updated to reflect the new contract.

Closes #36.
API7 EE rejects credentials whose `name` is empty (openapi
ConsumerCredential.required includes name; common.yaml Name is
minLength: 1). After the previous commit the CLI started forwarding
positional [id] as the URL path on PUT but stopped putting anything
in the `name` field, so the e2e suite hit 400 Bad Request on
`a7 credential create <id> --consumer ... --plugins-json ...`.

Mirror the id into `name` when the caller did not pass --name (or did
not supply `name:` in a --file payload). This matches the pre-fix
ergonomics for callers who treated the positional as both id and
name. Explicit --name still wins.

Adds two unit tests for the auto-derive behavior on both code paths.
@shreemaan-abhishek shreemaan-abhishek force-pushed the fix/credential-create-forward-id-via-put branch from cbd4e7e to e97d665 Compare May 26, 2026 06:59
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a7 credential create [id] so that a user-supplied credential id is honored by sending an upsert PUT /apisix/admin/consumers/<user>/credentials/<id> (instead of POST + server-generated UUID), aligning CLI behavior with the help text and closing #36. It also introduces a dedicated --name flag to set the credential display name independently of the id.

Changes:

  • Forward positional [id] (and id: from --file) as the credential id via PUT, falling back to POST when omitted.
  • Add --name flag and adjust create-time name derivation (mirror id into name when required and --name is absent).
  • Update unit tests and documentation to reflect new id/name semantics and precedence rules.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
pkg/cmd/credential/create/create.go Implements PUT-vs-POST submission based on provided id; adds --name; refactors request submission flow.
pkg/cmd/credential/create/create_test.go Updates and expands tests to cover id forwarding via PUT, precedence, and invalid id handling.
docs/user-guide/credential.md Documents new [id] behavior, --name, and updated examples.
docs/ga-test-report.md Updates GA test report notes to reflect the corrected positional id behavior and new --name flag.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pkg/cmd/credential/create/create.go Outdated
Comment on lines 86 to 95
id := opts.ID
if id == "" {
if raw, ok := payload["id"]; ok {
idStr, ok := raw.(string)
if !ok || strings.TrimSpace(idStr) == "" {
return fmt.Errorf("credential id must be a non-empty string")
}
id = idStr
}
}
// gave us an id but no explicit name, mirror the id into the name.
if _, hasName := payload["name"]; !hasName && id != "" {
payload["name"] = id
}
return "", false, fmt.Errorf("credential name must be a non-empty string")
}
return name, true, nil
return cmdutil.NewExporter(format, opts.IO.Out).WriteAPIResponse(raw)
Addresses three review comments on #48:

- Escape `opts.Consumer`, `id`, and `ggID` before interpolating them
  into the credential URL. Without escaping, special characters (`/`,
  `?`, `&`, `%`) could re-route the request or corrupt the query
  string.
- Validate `payload["id"]` from a `--file` payload whenever it is
  present, not only when the positional override is empty. A bogus
  value (non-string or whitespace-only) is now surfaced even when
  the positional supplies the URL segment.
- Validate `payload["name"]` (after `--name` override and id->name
  defaulting) so a non-string or whitespace-only name from `--file`
  errors out client-side with a clear message instead of falling
  through to a less specific server-side error.
- Normalize positional and file id/name with `strings.TrimSpace` so
  `id: "  foo "` produces `PUT /credentials/foo`, not the original
  whitespace.

The `stringField` helper consolidates the present/string/non-empty
check and writes the trimmed value back to the payload.

Tests cover URL escaping (consumer with `/`, id with space, gateway
group with `&`), file id rejection when the positional overrides,
trimming of whitespace in the file id, and rejection of a non-string
name in the file payload.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/cmd/credential/create/create.go`:
- Around line 139-146: The current logic trims opts.Name into name and silently
allows a whitespace-only --name to become empty when no id is supplied; change
the validation so that if opts.Name is non-empty (the user passed --name) but
strings.TrimSpace(opts.Name) yields an empty string and we're not in the file
flow (i.e., id == ""), the command returns an error instead of proceeding.
Locate the block that sets id := strings.TrimSpace(opts.ID) and name :=
strings.TrimSpace(opts.Name) (and constructs api.Credential{Name: name, Desc:
opts.Desc}) and add a guard that checks opts.Name != "" && name == "" && id ==
"" and returns a user-facing error asking for a non-empty name.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 78c5d5df-a454-4905-96cf-fec3dfc22a02

📥 Commits

Reviewing files that changed from the base of the PR and between b47eefd and 5639c21.

📒 Files selected for processing (4)
  • docs/ga-test-report.md
  • docs/user-guide/credential.md
  • pkg/cmd/credential/create/create.go
  • pkg/cmd/credential/create/create_test.go
✅ Files skipped from review due to trivial changes (1)
  • docs/ga-test-report.md

Comment on lines +139 to +146
id := strings.TrimSpace(opts.ID)
name := strings.TrimSpace(opts.Name)
if name == "" && id != "" {
// API7 EE requires a non-empty `name`. Mirror the id when the caller
// did not pass --name explicitly.
name = id
}
bodyReq := api.Credential{Name: name, Desc: opts.Desc}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Reject whitespace-only --name in non-file flow.

Line 140 trims --name, but if a user passes whitespace-only input and no positional id, the request proceeds with an empty name instead of failing fast (file flow rejects this case).

Suggested patch
 id := strings.TrimSpace(opts.ID)
 name := strings.TrimSpace(opts.Name)
+if opts.Name != "" && name == "" {
+	return fmt.Errorf("credential name must be a non-empty string")
+}
 if name == "" && id != "" {
 	// API7 EE requires a non-empty `name`. Mirror the id when the caller
 	// did not pass --name explicitly.
 	name = id
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
id := strings.TrimSpace(opts.ID)
name := strings.TrimSpace(opts.Name)
if name == "" && id != "" {
// API7 EE requires a non-empty `name`. Mirror the id when the caller
// did not pass --name explicitly.
name = id
}
bodyReq := api.Credential{Name: name, Desc: opts.Desc}
id := strings.TrimSpace(opts.ID)
name := strings.TrimSpace(opts.Name)
if opts.Name != "" && name == "" {
return fmt.Errorf("credential name must be a non-empty string")
}
if name == "" && id != "" {
// API7 EE requires a non-empty `name`. Mirror the id when the caller
// did not pass --name explicitly.
name = id
}
bodyReq := api.Credential{Name: name, Desc: opts.Desc}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/cmd/credential/create/create.go` around lines 139 - 146, The current
logic trims opts.Name into name and silently allows a whitespace-only --name to
become empty when no id is supplied; change the validation so that if opts.Name
is non-empty (the user passed --name) but strings.TrimSpace(opts.Name) yields an
empty string and we're not in the file flow (i.e., id == ""), the command
returns an error instead of proceeding. Locate the block that sets id :=
strings.TrimSpace(opts.ID) and name := strings.TrimSpace(opts.Name) (and
constructs api.Credential{Name: name, Desc: opts.Desc}) and add a guard that
checks opts.Name != "" && name == "" && id == "" and returns a user-facing error
asking for a non-empty name.

@shreemaan-abhishek shreemaan-abhishek merged commit 24a54d7 into master May 26, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

credential: positional [id] is ignored — clarify or fix

2 participants